refactor: migrate from ts-node to native Node ESM#5265
Merged
Conversation
Contributor
Preview deploymentsHost Test Results 1 files + 1 1 suites +1 1h 55m 1s ⏱️ + 1h 55m 1s Results for commit 9c55e1a. ± Comparison against earlier commit 0cbf225. Realm Server Test Results 1 files ±0 1 suites ±0 12m 12s ⏱️ +26s Results for commit 9c55e1a. ± Comparison against earlier commit 0cbf225. |
7651d64 to
5acc18b
Compare
habdelra
approved these changes
Jun 19, 2026
0e42e47 to
0cbf225
Compare
Native Node 24 type-stripping is the target runtime for the ts-node migration. Set engines.node to ">=24" at the root and align packages that still declared older floors (boxel-cli was ">= 18"; host and eslint-plugin-boxel were ">= 20"). Add a root .nvmrc matching the existing .mise.toml pin (24.13.1), and bump the one stray CI setup-node from 20 to 24. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Native Node ignores ts-node's resolver: it reads package.json exports and will neither fall back to index.js nor extension-resolve bare specifiers. Add an exports map pointing at the .ts sources so both native Node and the host's Vite/Embroider build resolve the package and its subpaths (bare entry, explicit directory-index subpaths, and a "./*" -> "./*.ts" wildcard); keep the existing #fetch/#lint-task imports. Do not set "type":"module" — the package ships CJS .js eslint configs that would break, so Node's per-file syntax detection handles the mix. Replace the runtime constructs native Node rejects under ESM: the lazy require() in fetch-node.ts (now createRequire) and url-signature.ts (now a bare crypto import, which the host already aliases to crypto-browserify), and __dirname -> import.meta.dirname. Flip the bench:amd scripts and their dynamic requires to native node, drop the ts-node devDependency, and drop the now-redundant .ts extension on the marked-sync shim import in the host so it resolves through the exports map. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e ESM The host's Vite/Embroider build adds extensions and index-resolves, so it hid import forms that native Node's ESM resolver rejects. Make every import in runtime-common resolvable by native Node (verified by importing the package and all subpaths under `node`, no bundler): - Add `.js` to extensionless CJS-package subpath imports that have no exports map (lodash/<method>, matrix-js-sdk/lib/utils — the latter has both a utils.js file and a utils/ directory, and ESM picks the dir). - Replace the named import from lodash's CJS root in catalog.ts with per-method default imports (Node can't statically detect lodash's CJS named exports). - Point the three bare-directory barrel imports (`from '.'`) at `./index.ts` (native ESM does not index-resolve directories). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
postgres is the first leaf consumer of runtime-common to run on native
Node. Add an exports map pointing at its .ts sources (".", "./*", and
"./package.json") so consumers resolve the package and its subpaths
without ts-node; replace __dirname with import.meta.dirname in
pg-adapter.ts and convert-to-sqlite.ts; flip the migrate and make-schema
scripts from ts-node to native node; and drop the ts-node dependency.
Leave "type" unset so Node's syntax detection keeps the package's CJS
.js migration helpers working alongside the ESM .ts sources.
Verified by importing @cardstack/postgres (and its runtime-common
dependency chain) under native node.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Foundation toward the full node-run ESM migration: type:module on
runtime-common + postgres, nested package.json {type:commonjs} for their
CJS dirs (etc/eslint, migrations), .cjs renames for stray CJS files, and
revert of the collateral host/eslint-plugin engines bumps. Resolves
TS1470 but TS2307 (base path aliases) and TS2349 (CJS interop) remain,
plus all consumers still need migrating. Parked — not green.
Swap ts-node for native Node (>=24) TypeScript execution across the
node-run package cluster: runtime-common, postgres, billing,
realm-server, realm-test-harness, ai-bot, bot-runner, matrix, and
software-factory. All five service entries plus ai-bot and bot-runner
now link their full module graph under native node.
Native Node ESM is stricter than ts-node/Vite about resolution; the
recurring breakages and their fixes are captured as a reusable,
idempotent codemod under scripts/esm-codemod/ (run.mjs orchestrator,
per-rule modules, and README.md documenting the full error taxonomy):
- add explicit extensions to relative imports (no extension search)
- switch lodash -> lodash-es named imports repo-wide
- CJS named imports (fs-extra, debug) -> default + destructure
- __dirname/__filename -> import.meta.dirname/filename
- ts-node --transpileOnly <entry> -> node <entry>.ts in
package.json / *.sh / mise-tasks
Manual fixes not suited to a blind codemod: exports maps + type:module
on billing and realm-test-harness; in-source spawn('ts-node', ...) ->
spawn('node', ['x.ts', ...]); postgres .js->.cjs import; matrix-js-sdk
deep-import .js suffixes; and an import.meta.url-as-path bug in lint.ts.
Still remaining: the qunit test-runner bootstrap (qunit --require
ts-node/register -> node tests/index.ts) and removing the ts-node
devDependency afterward.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace `qunit --require ts-node/register/transpile-only tests/index.ts`
with `node tests/index.ts` for ai-bot, bot-runner, software-factory, and
realm-server. The qunit CLI used to supply the QUnit global, TAP reporter,
autostart, and a failure-based exit code; each package now has a
tests/qunit-bootstrap.ts that wires those up (autostart off, TAP reporter,
runEnd exit code) and its tests/index.ts ends with QUnit.start().
Test files imported `{ module, test }` from qunit, which Node's ESM loader
can't read as named exports from the CJS package; the cjs-named-to-default
codemod (now line-anchored so it skips qunit imports embedded in
test-fixture strings) rewrites them to a default import + destructure.
Also adds jsonwebtoken to that codemod's CJS list.
realm-server's tests/index.ts additionally needed a createRequire shim so
its synchronous, order-preserving test-file loader keeps working, with an
explicit `.ts` on each require (native require does no extension search);
its CI JUnit reporter is renamed .cjs (realm-server is now type:module).
Removes the now-unused ts-node devDependency from every package and fixes
two `#!/usr/bin/env ts-node` shebangs and a `ps | grep ts-node`
process-matcher that no longer matches the node-run children.
Known remaining: ai-bot has 5 tests that reassign sealed ES-module
namespace bindings for mocking (needs DI); software-factory's full node
suite needs boxel-cli and its own src migrated off bare require().
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two ESM-semantic issues surfaced once ai-bot ran under `node tests/index.ts`: 1. node-pg-migrate default-export interop (postgres). `import migrate from 'node-pg-migrate'` binds the whole CJS namespace under native ESM, so the runner (on `.default`) wasn't callable — "migrate is not a function". This broke DB migration everywhere, not just tests. Resolve `.default ?? ns`. Also add `package.json` to the migrate `ignorePattern` so the `type:commonjs` marker file in the migrations dir isn't loaded as a migration. 2. Sentry mocking against a sealed ES-module namespace. Tests reassigned `(Sentry as any).captureException`, a no-op because `import * as Sentry` bindings are read-only. Route the source through a mutable `lib/sentry.ts` `errorReporter` indirection that responder.ts/debug.ts call and the tests stub. ai-bot now passes 170/170 (locking tests need the usual PG env vars). Documents both as error classes #12 and #13 in the codemod README. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
software-factory's node test suite crashed at boot on `require('@cardstack/logger')`
in src/logger.ts — `require` doesn't exist in ESM scope. @cardstack/logger is a
CJS package that ships no type declarations, so the existing code cast the
`require()` result; keep that by defining `require` via createRequire rather than
switching to a static import (which would trip TS7016). Same fix for the
identical realm-test-harness/src/logger.ts.
With this and the previously-added @cardstack/boxel-cli `exports` map, the
software-factory node suite runs: 452/453 pass. The one failure
(port-allocator dual-stack `::` bind) is a macOS-vs-Linux socket-semantics
difference, not a migration regression — it asserts Linux behavior and fails the
same way under ts-node on macOS.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Packages flipped to "type":"module" (billing, realm-server, realm-test-harness)
treat their `.eslintrc.js` as ESM, so ESLint fails to load them
("module is not defined in ES module scope"). Rename those configs to
`.eslintrc.cjs`, which ESLint still auto-discovers.
Same problem for realm-server's CJS helper scripts run via `node`:
`shard-test-modules.js` (invoked in CI to compute test shards) and
`run-test-modules.js` both use `require`. Rename to `.cjs` and update the CI
workflow reference.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…odemod README Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
setup-localhost-resolver.ts (undici/dns, gated on BOXEL_ENVIRONMENT) and lib/wtfnode-on-signal.ts (wtfnode, gated on BOXEL_WTFNODE) call `require`, which doesn't exist in ESM scope — they'd throw the moment their gate is set. Recreate `require` via createRequire inside each guard, preserving the lazy, optional-dependency semantics (matching the existing fetch-node.ts pattern). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Flipping the node cluster to "type":"module" switches ember-tsc's nodenext resolution from CJS-mode to ESM-mode, which surfaced the parked-WIP type errors. Root causes and fixes: - Base-realm path alias (the bulk): under ESM-mode nodenext the extensionless `"https://cardstack.com/base/*": ["../base/*"]` mapping no longer resolves (.gts/.ts need explicit extensions). Expand every tsconfig's mapping to try `*.gts`/`*.ts`/`*.d.ts`/`*`. This also clears the transitive TS2307 cascade in consumers (catalog, host, …) that type-check runtime-common's now-ESM sources. - CJS default-interop the nodenext type layer can't model even though it works at runtime: bump magic-string 0.25.9 -> ^0.30.21 (properly packaged exports+types; AMD transpiler output verified unchanged); re-export `ignore` once with its call signature (runtime-common/ignore.ts); type node-pg-migrate's runner via RunnerOption. - Make ai-bot/bot-runner/software-factory/matrix "type":"module" (they already run as ESM and use import.meta, which CJS-mode rejected with TS1470); switch matrix's tsconfig to nodenext + skipLibCheck; rename their CJS .eslintrc.js -> .cjs. - Misc: `import type` from a bare `utils/jwt` -> relative; matrix-js-sdk type deep-import gains .js; QUnit.reporters/on cast (missing from @types/qunit); drop a now-conflicting `declare const QUnit`; sweep 3 catalog cards to lodash-es. All nine node-cluster packages now type-check clean (0 errors): runtime-common, postgres, billing, realm-server, realm-test-harness, ai-bot, bot-runner, software-factory, matrix. Suites still pass (ai-bot 170, bot-runner 38) and the service entries still load. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…od README Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CI surfaced two gaps from the lodash-es switch and the require shims: - base, boxel-ui, and experiments-realm import lodash-es but never declared it (the dep swap only covered packages that previously declared `lodash`), so their builds/type-checks failed to resolve the module. Add `lodash-es` (boxel-ui also swaps `@types/lodash` -> `@types/lodash-es`). - realm-server lint:js: prettier-wrap the multi-line CJS destructures the interop codemod emitted, and drop the now-unused `eslint-disable @typescript-eslint/no-var-requires` directives left above the createRequire-shimmed `require()` calls. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two more CI failures from the native-ESM move: - Build / Realm Performance Benchmark: the host build's getLatestSchemaFile() (config/environment.js) picked the lexically-last entry in the migrations and schema dirs. The migrations dir now contains `package.json` (pins it to type:commonjs), which sorts after the timestamped migrations and made the freshness check compare `package.json` against the schema timestamp → false "schema out of date". Filter both lists to timestamped files only. - AMD Transpile Bench: `if (require.main === module)` entry-point guards throw `require is not defined` under ESM. Replace with `import.meta.main` (Node 24) across the affected scripts in runtime-common, realm-server, matrix, and boxel-cli. AMD bench gate passes locally (also confirms magic-string 0.30 transpile output is within the perf tolerance). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…uild boxel-cli is a published CJS package (esbuild emits format:cjs), but the ts-node->node conversion of its dev/build scripts made node run them as ESM (import syntax + type:unset), so their `__dirname`/`require.main` broke: - Convert the build scripts (build.ts, build-types.ts, build-test-harness.ts, build-realms.ts, build-skills.ts, build-plugin.ts) to import.meta.dirname / import.meta.main. Leave src/ on __dirname — esbuild bundles it to CJS, where __dirname is correct and import.meta would be empty. - tsconfig -> module:esnext + moduleResolution:bundler so the now-ESM scripts' import.meta type-checks (the package stays CJS-published; tsconfig only drives lint:types, esbuild owns the build). - `start` now runs the built dist (node dist/index.js) since src is no longer directly node-runnable as ESM. Also fix runtime-common/ignore.ts to import the `ignore` CJS package via a namespace (`.default ?? ns`) instead of a default import: esbuild bundling downstream consumers (vscode-boxel-tools) couldn't synthesize the default. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…r boot The ts-node->node codemod's entry capture grabbed the closing `)` of a `$(ts-node … secret)` command substitution and appended `.ts` after it: MATRIX_REGISTRATION_SHARED_SECRET=$(node "…/matrix-registration-secret.ts").ts so the secret value was suffixed with a literal `.ts`. The realm-server then failed to come up, never bound :4201, and every integration suite that needs the dev-services stack (Host / Realm-Server / Matrix / Live Tests and downstream) cascaded. Verified locally: with the secret restored, the realm-server starts under native node and serves :4201 (base/skills modules + indexing). Fix the three mise service tasks (realm-server, test-realms, realm-server-base) and harden the codemod's entry pattern to exclude shell metacharacters ( ) ; & | ) so a command substitution can't be mis-captured again. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three native-ESM gaps the CI integration stack surfaced, each cascading widely:
- handlers/handle-indexing-dashboard.ts used `require.resolve('morphdom/…')` at
module load. `require.resolve` (no `require(` token, no `require.main`) slipped
past the earlier sweeps; it threw `require is not defined` and crashed the
worker-manager on import -> no indexing -> realm-server never reached :4201
readiness. Resolve via createRequire.
- matrix synapse start (support/synapse/index.ts, support/docker.ts) and
realm-server utils/jwt.ts imported CJS packages as `import * as fse/jsonwebtoken`
and then read members off the namespace. Under native ESM those members live on
the CJS default, so `fse.stat`/`sign`/`verify` were `undefined` — synapse failed
to start (`fse.stat is not a function`) and JWT signing/verification would no-op.
Switch to default imports.
Verified each member resolves under native node; matrix and realm-server
type-check clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ESM migration switched realm-served code to import from lodash-es, but the VirtualNetwork shim was still registered under the old 'lodash' id. Card code importing lodash-es found no shim and fell through to a network fetch (https://packages/lodash-es), failing every prerender render and wedging the base realm readiness check. Update the shim id to lodash-es and the matching expected-dependency lists in the realm indexing test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The migrations dir now carries a package.json pinning it to type:commonjs (so the CJS migration files keep working under the package's new type:module). node-pg-migrate's CLI default ignore pattern only skips dotfiles, so `pnpm migrate` loaded that package.json as a migration named "package" with no up/down function and threw "Unknown value for direction: up". Pass the same --ignore-pattern the programmatic runner in pg-adapter.ts already uses, so the CLI path (DB setup, CI migrate steps) skips package.json and .eslintrc.js too. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The migrations dir carries a package.json pinning it to type:commonjs (so the CJS migration files keep working under the package's new type:module). node-pg-migrate's CLI default ignore pattern only skips dotfiles, so the test-pg seed builder loaded that package.json as a migration named "package" with no up/down function and threw "Unknown value for direction: up", failing realm-server DB setup in CI. 4ae9312 fixed the `pnpm migrate` package.json script the same way, but the create_seeded_db.sh seed builder has its own inlined node-pg-migrate invocation that was missed. Pass the same --ignore-pattern so the seed path skips package.json and .eslintrc.js too. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`import * as QUnit from 'qunit'` yields an ESM namespace whose `.assert` is undefined under native Node ESM (qunit is CJS; the real object is the default export). ts-node's esModuleInterop previously masked this. The helper's `QUnit.assert.codeEqual = codeEqual` then threw "Cannot set properties of undefined (setting 'codeEqual')" at import time, aborting the entire realm-server test suite. Switch to the default import (`import QUnit from 'qunit'`) — the form every realm-server test file already uses — so `.assert` resolves. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`import QUnit, { module, test } from 'qunit'` fails under native Node
ESM with "The requested module 'qunit' does not provide an export named
'module'" — qunit is CJS and exposes module/test as properties of the
default export, not as ESM named exports. Because tests/index.ts
requires every test file at load time (TEST_MODULES only filters which
run), this one file aborted all six realm-server shards.
Destructure module/test from the default import, matching the 142 other
realm-server test files that already use `import QUnit from 'qunit'`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The realm-watch-stop integration fixture spawned a child that called
`require('ts-node').register(...)` to load watch-process-registry.ts
from source. ts-node was removed in the ESM migration, so the child
threw "Cannot find module 'ts-node'", exited 1, and both
realm-watch-stop tests failed with "fake watcher exited early".
Node 24 strips TypeScript types natively, so the registry .ts loads via
a plain require with no loader. Verified the fixture boots
(FAKE_WATCHER_READY, registers, stays alive) and all three tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ts-node was dropped from every package's deps, but invocations, a broken
process-kill pattern, and stale comments/docs lingered. Sweep them:
Functional
- boxel-cli `bin/boxel.js`: the dev fallback `require('ts-node')` is gone;
the CLI ships as a built CJS bundle (source uses CommonJS-only globals
like __dirname and isn't native-ESM-runnable), so error clearly when
dist is missing instead of throwing "Cannot find module 'ts-node'".
- `mise-tasks/lib/dev-common.sh`: the orphan-sweep matched
`node_modules.*--transpileOnly (worker|main|prerender)`, which never
matches the native `node main.ts` / `node worker-manager.ts` /
`node prerender/*-server.ts` processes — so `mise run kill-all` and dev
cleanup leaked the realm/worker/prerender ports. Match the native entries
(by name + a distinctive flag) instead.
- `packages/postgres/Dockerfile`: ts-node → node for fix-migration-names.ts.
- `.github/workflows/boxel-cli-publish.yml`: ts-node → node (3 sites).
- delete `packages/realm-server/scripts/run-test-modules.cjs` — an unused
qunit runner that still bootstrapped ts-node (the suite runs via
run-qunit-with-test-pg.sh / `node tests/index.ts`).
Docs / comments / log strings
- start-*.sh: "about to exec ts-node" log lines now say node; header
comments drop the ts-node tsconfig/binary rationale.
- mise service comments, worker-manager.ts, opencode.ts, find-package-root.ts,
fixtures.ts, context-search + canusetool usage strings, boxel-cli + ai-bot
READMEs, and the indexing-diagnostics skill: ts-node → node.
Left intentionally: the esm-codemod tooling, accurate "without ts-node"
notes, pg-adapter's historical esModuleInterop rationale, and the
phase-1-plan historical design doc.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up to the ts-node cleanup: the last references were explanatory comments/docs, not usage. Reword them to describe the native-Node behavior directly instead of contrasting with ts-node. - .eslintrc.js / erasable-syntax-selectors.cjs: describe the "erasable TS for --experimental-strip-types" rule without naming ts-node. - pg-adapter.ts: the node-pg-migrate `.default` unwrap is still required (native ESM binds the CJS namespace object; the runner is on `.default`) — keep the cast, but explain it purely in native-ESM terms. - fake-watcher fixture + software-factory phase-1 plan: native `node` wording, no ts-node. Only scripts/esm-codemod/** still mentions ts-node, by design (it's the migration tooling). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t tests) The two `/_post-deployment` maintenance-endpoints tests stubbed `compareCurrentBoxelUIChecksum` / `writeCurrentBoxelUIChecksum` on the `import * as` namespace of boxel-ui-change-checker. Under native ESM that namespace is read-only, so sinon threw "ES Modules cannot be stubbed" and both tests failed (the only red in realm-server shard 4). Group the two functions on an exported mutable object `boxelUIChecker` and have the post-deployment handler call through it; the tests stub the object's methods. This matches the codebase's stubbing convention (stub methods on objects/instances — global.fetch, stripe.subscriptions — never module namespaces) and preserves the handler's behavior. Verified the stub intercepts calls through the object under native node. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Env-mode CI (host shards, Live Tests, Software Factory) drives the
prerender through puppeteer, which in env-mode relies on puppeteer's
bundled Chrome (env-vars.sh only points PUPPETEER_EXECUTABLE_PATH at a
system Chrome in non-env-mode). actions/cache caches the pnpm store but
not ~/.cache/puppeteer, and on a store cache hit puppeteer's postinstall
skips the Chrome download — so the prerender threw "Could not find Chrome
(148.0.7778.97)", the standby pool stayed empty, base indexing never
finished, and /base/_readiness-check hung (host shards timed out at
000000). Main only passed because its warm cache still had Chrome.
- init: explicitly `puppeteer browsers install chrome` after install
(idempotent; no-op when already cached) so every job that runs the
prerender has Chrome regardless of cache state.
- Software Factory: `playwright install --with-deps` so the apt system
libraries are present too ("Host system is missing dependencies to run
browsers").
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`pnpm --filter <pkg> exec puppeteer browsers install chrome` went through pnpm's recursive-exec path and dropped the `chrome` arg in CI (puppeteer printed its usage and exited 1), which failed the init step for every workflow. Use a non-recursive `pnpm exec` from the package directory instead — the same form SF's `pnpm exec playwright install` uses, which forwards args correctly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
puppeteer's postinstall leaves an empty browser folder (~/.cache/puppeteer/chrome/linux-<ver>) with the download skipped, so the prerender fails "Could not find Chrome" and a plain `browsers install` refuses with "folder exists but executable is missing — All providers failed". Remove any partial install first, then download cleanly (the CDN works — main's postinstall downloads the same version fine). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Abandon the bundled-Chrome approach for env-mode CI — puppeteer's postinstall leaves an empty cache folder and an explicit `browsers install` wouldn't reliably land Chrome where the prerender looks, so the env-mode prerender kept failing "Could not find Chrome". env-vars.sh already points PUPPETEER_EXECUTABLE_PATH at the system Chrome in standard mode; the block was just inside the non-env-mode guard. Move it to the "regardless of env-mode" section so env-mode uses the runner's /usr/bin/google-chrome too (it exists — the non-env-mode jobs already use it). Gated on the binary existing and the path being unset, so it's a no-op-or-correct in env-mode prod (the prerender container installs google-chrome-stable, and an explicit override still wins). Revert the init browser-install step it replaces. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…l module
@cardstack/boxel-cli/api mapped to raw api.ts source. That resolves under
some loaders (vitest, plain node via the workspace symlink realpath) but not
Playwright's ESM worker loader, which failed loading software-factory's
.spec.ts files with "does not provide an export named 'BoxelCLIClient'",
taking down every SF Playwright shard.
Bundle api.ts to dist/api.js (esbuild CJS — named exports stay detectable by
Node's cjs-module-lexer, so ESM consumers keep `import { BoxelCLIClient }`)
and point exports["./api"] at it; the types condition still resolves to
api.ts, so type resolution is unchanged and no .d.ts is needed. Add a
`build:api` script and run it before software-factory's node + playwright
tests so the built module is present (only SF consumes this subpath).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`pnpm --filter @cardstack/boxel-cli build:api` runs only buildAPI() to
rebuild the API surface quickly for cross-package test consumers
(software-factory). But the bundled dist/api.js imports content-tag
transitively, and content-tag reads its wasm with
`${import.meta.dirname}/content_tag_bg.wasm` — i.e. from boxel-cli's
dist/. The wasm copy lived inside buildCLI() and only ran on a full
build, so software-factory's `test:node` / `test:playwright` saw
ENOENT on dist/content_tag_bg.wasm.
Hoist the copy into a standalone step that always runs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Same root cause as eb42f81 / 40ad5cf — qunit is CJS and exposes module/test as properties of the default export, not as ESM named exports. Under native Node ESM, `import { module, test } from 'qunit'` throws "does not provide an export named 'module'". Because tests/index.ts requires every test file at load time (TEST_MODULES filters which run), this one file aborted all six realm-server shards. Match the convention used by the other realm-server test files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Native Node ESM doesn't expose `__filename`. The sibling realm-server tests use `basename(import.meta.filename)` for the same module label; match that convention. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After rebasing onto main (which bumped puppeteer to 25.0.2 and removed ts-node from realm-test-harness), `pnpm install` strips the residual ts-node entries that lingered in pnpm-lock.yaml from the pre-rebase state. No package.json changes — just lockfile alignment. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0cbf225 to
9c55e1a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Migrates the node-run package cluster off
ts-nodeand onto native Node (≥24) TypeScript execution (type-stripping):runtime-common,postgres,billing,realm-server,realm-test-harness,ai-bot,bot-runner,software-factory, andmatrix.All service entry points load under native node, the qunit test suites run via
node tests/index.ts, andlint:typesis green across all nine cluster packages.Linear: CS-11449
Warning
Draft. The realm-server full integration suite still needs to run against the dev-services stack in CI, and host
lint:typeswants CI confirmation (configured the same as the now-greencatalog). See "Remaining" below.What changed
Runtime
ts-node --transpileOnly <entry>→node <entry>.tsacrosspackage.jsonscripts, shell scripts, mise-tasks, and in-sourcespawn(...)sites; removed thets-nodedevDependency.exportsmaps +"type":"module"to the cluster packages so native Node can resolve their.tsentry points.lodash→lodash-esrepo-wide (native ESM named imports; tree-shakeable under Vite too).fs-extra,debug,jsonwebtoken,node-pg-migrate),__dirname/__filename→import.meta, andcreateRequireshims for lazyrequire().Test runners
node tests/index.tswith aqunit-bootstrap(autostart off, TAP reporter, failure-based exit code) replacing the qunit CLI +ts-node/register.Type-checking (
lint:types)nodenext(models the native-node runtime rather than masking it likebundlerwould)..gts/.tsunder ESM-mode resolution; this also clears the transitive cascade in consumers.magic-string0.25.9 → ^0.30.21 (properly packaged); AMD transpiler output verified unchanged.Tooling
scripts/esm-codemod/(run.mjs+ per-rule modules) and aREADME.mddocumenting the full error taxonomy and the manual fixes.Verification
lint:typesclean (0 errors).ai-bot170/170,bot-runner38 pass + 1 skip,software-factory452/453 (the one failure is a macOS-vs-Linux dual-stack socket test, not a migration regression).realm-servermain/worker/worker-manager/prerender/manager,ai-bot,bot-runner) link their full module graph under native node.Remaining before un-drafting
realm-serverfull integration suite needs the dev-services stack (CI).hostlint:typesCI confirmation.🤖 Generated with Claude Code